Syntax
Overview
Karnak's function for drawing graphs is drawgraph(). This takes a single argument, a Graph, and tries to place it on the current Luxor drawing. It uses the current color, scale, and rotation, marking the vertices of the graph with circles.
@drawsvg begin
background("grey10")
sethue("darkcyan")
g = complete_graph(10)
drawgraph(g)
end 600 300To control the appearance of the graph, you supply values to the keyword arguments. Most keyword arguments accepts vectors, ranges, and scalar values, and a few accept functions as well.
Here's a contrived (and consequently hideously ugly) example of the type of syntax you can use:
@drawsvg begin
background("grey10")
sethue("purple")
g = smallgraph(:karate)
drawgraph(g, layout=stress,
vertexshapes = [:square, :circle],
vertexfillcolors = (v) -> v ∈ (1, 3, 6) ? colorant"red" : colorant"grey40",
vertexstrokecolors = colorant"orange",
vertexstrokeweights = range(0.5, 4, length=nv(g)),
vertexshapesizes = 2 .* [Graphs.outdegree(g, v) for v in Graphs.vertices(g)],
vertexlabelfontsizes = 2 .* [Graphs.outdegree(g, v) for v in Graphs.vertices(g)],
vertexlabels = 1:nv(g),
vertexlabelrotations = π/8,
vertexlabeltextcolors = distinguishable_colors(10)
)
end 600 300Here, the outdegree for each vertex (the number of edges leaving it) is used to control the size of the vertices and the font sizes too. vertexshapes flip-flops between squares and circles for each vertex shape, but the size of the shape is determined by a vertexshapesizes function, which receives a Vector of sizes, the outdegree values for each vertex. The font sizes of the labels are also set this way. A vertexfillcolors function lets you determine the shape's fill color for specific vertices, whereas the stroke color is always orange, with stroke weights gradually increasing. The colors of the labels are set by the Colors.distinguishable_colors() function passed to vertexlabeltextcolors. And all the labels are rotated, for no particularly good reason.
Usually, if a vector runs out before the vertices and edges have been drawn, some mod1 magic means the values repeat from the beginning again.
The BoundingBox
The graphics for the graph are placed to fit inside the current BoundingBox (ie the drawing), after allowing for the margin (the default is 30). You can pass a different BoundingBox to the boundingbox keyword argument.
Layout algorithms
The only clever part of this package is provided by NetworkLayout.jl), which is where you should look for information about the various algorithms that determine where vertices are positioned.
Here are some formulations which work:
layout = squaregrid
layout = shell
layout = stress
layout = spectral
layout = (g) -> spectral(adjacency_matrix(g), dim=2)
layout = shell ∘ adjacency_matrix
layout = (g) -> sfdp(g, Ptype=Float64, dim=2, tol=0.05, C=0.4, K=2)
layout = Shell(nlist=[6:10,])Alternatively, you can pass a vector of Luxor Points to the layout keyword argument. Vertices will be placed on these points, rather than at points suggested by the NetworkLayout functions.
For example, in this next drawing, the two sets of points for a bipartite graph are generated beforehand.
@drawsvg begin
background("grey20")
N = 12; H = 250; W = 550
g = complete_bipartite_graph(N, N)
pts = vcat(
between.(O + (-W/2, -H/2), O + (-W/2, H/2), range(0, 1, length=N)), # left set
between.(O + (W/2, H/2), O + (W/2, -H/2), range(0, 1, length=N))) # right set
circle.(pts, 1, :fill)
drawgraph(g, vertexlabels = 1:nv(g), layout = pts,
edgestrokeweights = 0.5,
edgestrokecolors = (n, f, t) -> HSB(rescale(n, 1, ne(g), 0, 360), 0.6, 0.9))
end 600 300The vertexfunction and edgefunction arguments
The two keyword arguments vertexfunction and edgefunction allow you to pass control over the drawing process completely to two functions, which can be anonymous functions.
vertexfunction = my_vertexfunction(vertex, coordinates)
edgefunction = my_edgefunction(from::Point, to::Point)These allow you to place graphics at coordinates[vertex], and to draw edges from from to to, using any available tools for drawing.
In the following picture, the vertex positions were passed to a function that placed clipped PNG images on the drawing (using Luxor.readpng() and Luxor.placeimage()), and the edges were drawn using sine curves. Refer to the Luxor documentation for more than you could possibly want to know about putting colored things on drawings.

It's also possible to draw graphs recursively if you use vertexfunction.
g = star_graph(8)
function rgraph(g, l=1)
if l > 3
return
else
drawgraph(g,
layout = stress,
vertexfunction = (v, c) -> begin
@layer begin
sethue(HSB(rescale(v, 1, 8, 0, 360), .7, .8))
translate(c[v])
circle(c[v], 5, :fill)
rgraph(g, l + 1)
end
end,
boundingbox = BoundingBox()/3)
end
end
@drawsvg begin
background("grey10")
rgraph(g)
end 800 600Vertex labels and shapes
The vertexlabels argument
Use vertexlabels to choose the text to associate with each vertex. Supply a range, array of strings or numbers, or a string.
This example draws all vertices, and numbers them from 1 to 6.
@drawsvg begin
background("grey10")
g = smallgraph(:octahedral)
sethue("gold")
drawgraph(g, layout=stress,
vertexlabels = 1:nv(g),
vertexshapesizes = 10
)
end 600 300You can use a function with vertexlabels to display a vertex; it should return a string to display.
@drawsvg begin
background("grey10")
g = smallgraph(:octahedral)
sethue("purple")
drawgraph(g, layout=stress,
vertexlabels = (v) -> v ∈ (1, 2, 3) && string(v),
vertexshapesizes = 30,
vertexlabelfontsizes = 30,
)
end 600 300vertexshapes and vertexshapesizes
To determine the shape of the graphic placed at a vertex, you can use these two keyword arguments.
Options for vertexshapes are :circle and :square. With just two in a vector, they will be used alternately.
@drawsvg begin
background("grey10")
g = smallgraph(:moebiuskantor)
sethue("gold")
drawgraph(g, layout=shell,
vertexshapes = [:square, :circle],
vertexshapesizes = [35, 20],
)
end 600 300Yes, it's a limited choice. But no worries, because you can pass a function to vertexshapes to draw any shape you like. The single argument is the vertex number; graphics will be centered at the vertex location.
@drawsvg begin
background("grey10")
g = smallgraph(:moebiuskantor)
sethue("cyan")
drawgraph(g, layout=shell,
vertexshapes = (v) -> star(O, 20, 5, 0.5, 0, :fill))
end 600 300In the next example, the sizes of the labels and shapes are determined by the degree of each vertex, supplied in a vector.
@drawsvg begin
background("grey10")
g = smallgraph(:karate)
sethue("slateblue")
drawgraph(g, layout=stress,
vertexfillcolors = (v) -> v ∈ (1, 3, 6) && colorant"red",
vertexlabels = 1:nv(g),
vertexlabelfontsizes=[Graphs.outdegree(g, v) for v in Graphs.vertices(g)],
vertexshapesizes=[Graphs.outdegree(g, v) for v in Graphs.vertices(g)])
end 600 300To show every other vertex, you could use something like this:
@drawsvg begin
background("grey10")
g = smallgraph(:truncatedcube)
sethue("slateblue")
drawgraph(g, layout=stress,
vertexlabels = ["1", ""],
vertexshapesizes = [10, 0])
end 600 300function whiten(col::Color, f=0.5)
hsl = convert(HSL, col)
h, s, l = hsl.h, hsl.s, hsl.l
return convert(RGB, HSL(h, s, f))
end
function drawball(pos, ballradius, col::Color;
fromlum=0.2,
tolum=1.0)
gsave()
translate(pos)
for i in ballradius:-0.25:1
sethue(whiten(col, rescale(i, ballradius, 0.5, fromlum, tolum)))
offset = rescale(i, ballradius, 0.5, 0, -ballradius/2)
circle(O + (offset, offset), i, :fill)
end
grestore()
end
@drawsvg begin
background("grey4")
g = grid((10, 10))
drawgraph(g,
layout = squaregrid,
edgelines = 0,
vertexshapes = (v) -> drawball(O, 25, RGB([Luxor.julia_red,Luxor.julia_purple, Luxor.julia_green][rand(1:end)]...))
)
end 600 600vertexshaperotations
@drawsvg begin
background("grey10")
g = smallgraph(:octahedral)
sethue("slateblue")
drawgraph(g, layout=stress,
vertexshapes = :square,
vertexshapesizes = 40,
vertexshaperotations = range(0, 2π, length = nv(g))
)
end 600 300vertexstrokecolors and vertexfillcolors
These keywords accept a Colors.jl colorant, an array of them, or a function that generates a color.
@drawsvg begin
background("grey10")
g = smallgraph(:cubical)
sethue("darkorange")
drawgraph(g, layout=stress,
vertexshapes = :square,
vertexshapesizes = 20,
vertexfillcolors = [colorant"red", colorant"blue"],
vertexstrokecolors = [colorant"blue", colorant"red"])
end 600 300This function should return a Colorant:
@drawsvg begin
background("grey10")
g = smallgraph(:icosahedral)
sethue("darkorange")
drawgraph(g, layout=spring,
vertexshapes = :square,
vertexshapesizes = 20,
vertexfillcolors = (v) -> HSB(rescale(v, 1, nv(g), 0, 359), 1, 1))
end 600 300@drawsvg begin
background("grey30")
sethue("orange")
g = grid((20, 20))
drawgraph(g,
layout = squaregrid,
vertexfillcolors =
[RGB(rand(), rand(), rand()) for i in 1:nv(g)])
end 600 600By now, I think you get the general idea. Try playing with the following keyword arguments:
vertexstrokeweightsvertexlabeltextcolorsvertexlabelfontsizesvertexlabelfontfacesvertexlabelrotationsvertexlabeloffsetanglesvertexlabeloffsetdistances
Being able to specify the font faces for vertex labels is of vital importance, of course, but difficult to demonstrate when the documentation is built on machines in the cloud with unknown typographical resources.
@drawsvg begin
background("grey10")
g = smallgraph(:pappus)
sethue("slateblue")
drawgraph(g,
vertexlabels = 1:nv(g),
vertexshapes = 0,
vertexlabelfontfaces = ["Times-Roman", "Courier", "Helvetica-Bold"],
vertexlabelfontsizes = 30)
end 600 300Edge options
edgefunction
As with vertexfunction, the edgefunction keyword argument allows you to do anything you like when the edges are drawn. Here, the calculated coordinates are extracted into a vector for later questionable purposes.
@drawsvg begin
background(0.1, 0.25, 0.15)
g = barbell_graph(22, 22)
A = Point[]
drawgraph(g, layout=stress,
edgefunction = (from, to) -> begin
push!(A, from),
push!(A, to)
end,
vertexshapes = :none)
setlinejoin("bevel")
setline(0.25)
@layer begin
scale(1.2)
for θ in range(0, 2π, length=6)
@layer begin
rotate(θ)
sethue(HSB(rescale(θ, 0, 2π, 90, 210), .8, .8))
poly(A, :stroke)
end
scale(0.8)
end
end
end 600 400Edge labels
Use edgelabels, edgelabelcolors, edgelabelrotations, etc. to control the appearance of the labels alongside edges. Here, the edgelabels keyword argument accepts a function with five, yes five, arguments: edge number, source, destination, from point, and to point, and is able to annotate each edge with its length in this representation.
@drawsvg begin
background("grey20")
g = smallgraph(:dodecahedral)
g = complete_graph(5)
fontsize(20)
drawgraph(g, layout=stress,
vertexshapes = :none,
edgestrokecolors = colorant"orange",
edgelabels = (k, src, dest, f, t) -> begin
@layer begin
sethue("white")
θ = slope(f, t)
text(string(round(distance(f, t), digits=1)),
midpoint(f, t),
angle=θ,
halign=:center)
end
end)
end 600 500edgelabels can also be a dictionary, where the keys are tuples, (src, dst), and the values are the text labels.
g = complete_graph(5)
edgelabeldict = Dict()
n = nv(g)
for i in 1:n
for j in 1:n
edgelabeldict[(i, j)] = string(i, " - ", j)
end
end
@drawsvg begin
background("grey20")
drawgraph(g, layout=stress,
vertexshapes = :circle,
vertexlabels = 1:n,
edgestrokecolors = colorant"orange",
edgelabelcolors = colorant"white",
edgelabels = edgelabeldict)
end 600 400The more code you're prepared to write, the more elaborate your labels can be:
sources = [1,2,1]
destinations = [2,3,3]
weights = [0.5, 0.8, 2.0]
g = SimpleWeightedGraph(sources, destinations, weights)
@drawsvg begin
background("grey10")
sethue("gold")
drawgraph(g,
vertexlabels = 1:nv(g),
vertexshapesizes = 20,
vertexlabelfontsizes = 30,
edgelabels = (edgenumber, edgesrc, edgedest, from, to) -> begin
@layer begin
sethue("black")
box(midpoint(from, to), 50, 30, :fill)
end
box(midpoint(from, to), 50, 30, :stroke)
text(string(get_weight(g, edgesrc, edgedest)),
midpoint(from, to),
halign=:center,
valign=:middle)
end)
end 600 300edgelist
This example draws the graph more than once; once with all the edges, and once with only the edges in edgelist, where edgelist is the path from vertex 15 to vertex 17, drawn in a sickly translucent yellow. The path is marked with X marks the spot cyan-colored shapes.
@drawsvg begin
background("grey10")
g = smallgraph(:karate)
sethue("slateblue")
drawgraph(g, layout = stress,
vertexlabels = 1:nv(g),
vertexshapes = :circle,
vertexshapesizes = 10,
vertexlabelfontsizes = 10)
astar = a_star(g, 15, 17)
drawgraph(g,
layout=stress,
vertexshapes = :none,
edgelist = astar,
edgestrokecolors=RGBA(1, 1, 0, 0.35),
edgestrokeweights=20)
drawgraph(g,
layout=stress,
edgelines=0,
vertexshapes = (v) -> v ∈ src.(astar) && polycross(O, 20, 4, 0.5, π/4, :fill),
vertexfillcolors = (v) -> v ∈ src.(astar) && colorant"cyan"
)
end 600 600edgelines
edgecurvature
edgestrokecolors
g = barbell_graph(3, 3)
@drawsvg begin
background("grey10")
fontsize(30)
sethue("white")
drawgraph(g,
layout=stress,
edgelabels = 1:ne(g),
edgecurvature = 10,
edgestrokeweights = 2 * (1:ne(g)),
edgelabelcolors = colorant"white",
edgestrokecolors=(edgenumber, from, to) ->
HSB(rescale(edgenumber, 1, ne(g), 0, 359), .8, .8)
)
end 600 500edgestrokeweights
One possible use for varying the stroke weight of the edges might be to indicate the weight of a weighted graph's edge:
wg = SimpleWeightedDiGraph(Graph(5, 10), 1.0)
for e in edges(wg)
add_edge!(wg, src(e), dst(e), rand(1:20))
end
@drawsvg begin
background("grey20")
sethue("gold")
drawgraph(wg,
edgecurvature=20,
vertexlabels = 1:nv(wg),
edgestrokeweights = [get_weight(wg, src(e), dst(e)) for e in edges(wg)])
end